﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Security.Cryptography;
using EncryptionLib.Users;
using EncryptionLib.Util;
using Ionic.Zip;
using System.Windows.Forms;
using System.Threading;

namespace EncryptionLib.Encryption
{
    public class EncryptionProgressArgs
    {
        public int Progress
        {
            get;
            set;
        }
        public Exception ProgressExeption
        {
            get;
            set;
        }

        public EncryptionProgressArgs()
        {
        }

        public EncryptionProgressArgs(int progress, Exception ex)
        {
            Progress = progress;
            ProgressExeption = ex;
        }


        public EncryptionProgressArgs(int progress)
        {
            Progress = progress;
        }
    }

    public delegate void EncryptionProgressHandler(object sender, EncryptionProgressArgs arg);

    /// <summary>
    /// Format of the output encrypted file:
    ///     HEADER | [FILENAME] | ENCRYPTED DATA
    ///     
    ///  HEADER: EncryptFileHeader
    ///  FILENAME: optional. Only has if we are encrypting one file and didn't zip the source data.
    ///  ENCRYPTED DATA
    /// </summary>
    public class Encryptor : IEncryptor
    {
        public const string ENCRYPTOR_VERSION = "1.0";

        public event EncryptionProgressHandler EncryptAsyncProgess;

        public List<string> Files
        {
            get;
            set;
        }

        public SymmetricAlgorithms Algorithm
        {
            get;
            set;
        }

        public UserData Reciever
        {
            get;
            set;
        }

        public string OutputFile
        {
            get;
            set;
        }

        public bool Zipped
        {
            get;
            set;
        }

        public CipherMode CipherMode
        {
            get;
            set;
        }

        public PaddingMode PaddingMode
        {
            get;
            set;
        }

        protected void OnEncryptAsyncProgess(Control context, EncryptionProgressArgs ex)
        {
            if (EncryptAsyncProgess == null)
                return;

            if(context.InvokeRequired)
            {
                context.Invoke(EncryptAsyncProgess, this, ex);
            }
            else
            {
                EncryptAsyncProgess(this, ex);
            }
        }

        public void EncryptAsync(Control context)
        {
            Thread trd = new Thread(new ParameterizedThreadStart(EncryptThread));
            trd.IsBackground = true;
            trd.Start(context);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <remarks>
        /// Exceptions:
        /// 
        ///  ArgumentException: 
        ///    File names was empty or cannot access to them,
        ///    and other cases...
        ///  CryptographicException:
        ///    some padding mode or cipherMode was not supported.
        /// </remarks>
        public void Encrypt()
        {
            DoEncrypt(null, false);
        }
                
        public Encryptor()
        {
            Files = new List<string>();
        }

        #region Utilities

        private void WriteEncryptedFile(EncryptFileHeader header, string sEncryptedFile, string sOutput)
        {
            FileStream fsOut = new FileStream(sOutput, FileMode.Create, FileAccess.Write);
            header.Serialize(fsOut);

            if(!header.Zipped)      // we are only encrypting one file, so file name is important here!
            {
                SerializerHelper.WriteString(Path.GetFileName(Files[0]), fsOut);
            }

            FileStream fsEncrypted = new FileStream(sEncryptedFile, FileMode.Open, FileAccess.Read);

            // copy encrypted data to the output file.
            const int BUFFERLEN = 4096;
            byte[] arrTmp = new byte[BUFFERLEN];
            fsEncrypted.Seek(0, SeekOrigin.Begin);
            fsOut.Seek(0, SeekOrigin.End);
            while(fsEncrypted.Position < fsEncrypted.Length)
            {
                int iRead = fsEncrypted.Read(arrTmp, 0, BUFFERLEN);
                fsOut.Write(arrTmp, 0, iRead);
            }

            fsOut.Flush();
            fsOut.Close();
            fsEncrypted.Close();
        }

        private EncryptFileHeader CreateHeader(byte[] key, byte[] iv)
        {
            // create secret key
            RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
            rsa.FromXmlString(Reciever.PublicKey);

            MemoryStream msPlainSecretKey = new MemoryStream();
            SerializerHelper.WriteBytes(key, msPlainSecretKey);
            SerializerHelper.WriteBytes(iv, msPlainSecretKey);

            MemoryStream msSecretKey = new MemoryStream();

            int BUFFER_SIZE = rsa.ExportParameters(false).Modulus.GetLength(0) - 11;
            
            msPlainSecretKey.Seek(0, SeekOrigin.Begin);
            msSecretKey.Seek(0, SeekOrigin.Begin);

            while (msPlainSecretKey.Position < msPlainSecretKey.Length)
            {
                byte[] arrInData = new byte[BUFFER_SIZE];
                int iRead = msPlainSecretKey.Read(arrInData, 0, BUFFER_SIZE);
                byte[] arrOutData;

                if (iRead > 0 && iRead != BUFFER_SIZE)
                {
                    arrInData = (byte[])SerializerHelper.ResizeArray(arrInData, iRead);
                }

                if (iRead > 0)
                {
                    arrOutData = rsa.Encrypt(arrInData, false);
                    msSecretKey.Write(arrOutData, 0, arrOutData.Length);
                }
            }
            msPlainSecretKey.Dispose();

            byte[] arrSecretKey = new byte[msSecretKey.Length];
            msSecretKey.Seek(0, SeekOrigin.Begin);
            int ii = msSecretKey.Read(arrSecretKey, 0, (int)msSecretKey.Length);
            Debug.Assert(ii == msSecretKey.Length);

            EncryptFileHeader ret = new EncryptFileHeader(Algorithm, CipherMode, PaddingMode, Zipped, arrSecretKey);
            ret.Version = ENCRYPTOR_VERSION;
            return ret;
        }

        private bool DESEncrypt(string sInput, string sOutput, out byte[] secretKey, out byte[] IV)
        {
            FileStream ms = null;
            FileStream msInput = null;
            CryptoStream cs = null;
            bool bRet = false;

            try
            {
                SymmetricAlgorithm algo = SymmetricAlgo.CreateSymmetricAlgorithm(Algorithm);
                algo.Mode = CipherMode;
                algo.Padding = PaddingMode;
                secretKey = algo.Key;
                IV = algo.IV;

                ms = new FileStream(sOutput, FileMode.Create, FileAccess.Write);
                cs = new CryptoStream(ms,
                   algo.CreateEncryptor(),
                   CryptoStreamMode.Write);

                msInput = new FileStream(sInput, FileMode.Open, FileAccess.Read);
                msInput.Seek(0, SeekOrigin.Begin);
                while (msInput.Position < msInput.Length)
                {
                    byte[] plainbytes = new byte[4096];
                    int iRead = msInput.Read(plainbytes, 0, 4096);
                    cs.Write(plainbytes, 0, iRead);
                }
                cs.FlushFinalBlock();

                bRet = true;
            }
            catch (System.Exception ex)
            {
                throw ex;
            }
            finally
            {
                if (cs != null)
                    cs.Close();
                if (ms != null)
                    ms.Close();
                if (msInput != null)
                    msInput.Close();
            }
            return bRet;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        /// <remarks>
        ///  Exceptions:
        ///   System.ArgumentNullException:
        ///     fileName is null.
        ///
        ///   System.Security.SecurityException:
        ///     The caller does not have the required permission.
        ///
        ///   System.ArgumentException:
        ///     The file name is empty, contains only white spaces, or contains invalid characters.
        ///
        ///   System.UnauthorizedAccessException:
        ///     Access to fileName is denied.
        ///
        ///   System.IO.PathTooLongException:
        ///     The specified path, file name, or both exceed the system-defined maximum
        ///     length. For example, on Windows-based platforms, paths must be less than
        ///     248 characters, and file names must be less than 260 characters.
        ///
        ///   System.NotSupportedException:
        ///     fileName contains a colon (:) in the middle of the string.
        /// </remarks>
        private string Zip()
        {
            string sRet = string.Empty;

            if (!Zipped && Files.Count == 1)
            {
                FileInfo fiTmp = new FileInfo(Files[0]);        // test if we can access this file.
                sRet = fiTmp.FullName;
            }
            else if((Zipped && Files.Count == 1) || Files.Count > 1)
            {
                Zipped = true;

                string sBaseFolder = string.Empty;
                for (int i = Files.Count - 1; i >= 0; --i)
                {
                    string sFolder = Path.GetDirectoryName(Files[i]);
                    if (string.IsNullOrEmpty(sBaseFolder))
                    {
                        sBaseFolder = sFolder;
                    }
                    else
                    {
                        // find the common path
                        for (int j = Math.Min(sBaseFolder.Length, sFolder.Length); j >= 0; --j)
                        {
                            string sPrefix = sFolder.Substring(0, j);
                            if (sBaseFolder.IndexOf(sPrefix) == 0)
                            {
                                sBaseFolder = sPrefix;
                                break;
                            }
                        }
                    }
                }

                sRet = Path.GetTempFileName();
                using(ZipFile zip = new ZipFile())
                {
                    zip.UseUnicodeAsNecessary = true;
                    foreach (string sFile in Files)
                    {
                        string sDir = Path.GetDirectoryName(sFile);
                        string sPathInZip = sDir.Substring(sBaseFolder.Length);
                        zip.AddFile(sFile, sPathInZip);
                    }
                    zip.Save(sRet);
                }

                //FileInfo[] arrFiles = new FileInfo[Files.Count];
                //for(int i = Files.Count - 1; i >= 0; --i)
                //{
                //    arrFiles[i] = new FileInfo(Files[i]);

                //    string sFolder = Path.GetDirectoryName(Files[i]);
                //    if(string.IsNullOrEmpty(sBaseFolder))
                //    {
                //        sBaseFolder = sFolder;
                //    }
                //    else
                //    {
                //        // find the common path
                //        for(int j = Math.Min(sBaseFolder.Length, sFolder.Length); j >= 0; --j)
                //        {
                //            string sPrefix = sFolder.Substring(0, j);
                //            if(sBaseFolder.IndexOf(sPrefix) == 0)
                //            {
                //                sBaseFolder = sPrefix;
                //                break;
                //            }
                //        }
                //    }
                //}
                //string sOutputFile = Path.GetTempFileName();

                //GZipResult ret = GZip.Compress(arrFiles, sBaseFolder, Path.GetDirectoryName(sOutputFile),
                //    Path.GetFileName(sOutputFile));

                //if(!ret.Errors)
                //{
                //    sRet = ret.ZipFile;
                //}
            }
            return sRet;
        }

        protected void DoEncrypt(Control context, bool bNotifyProgress)
        {
            if (bNotifyProgress)
                OnEncryptAsyncProgess(context, new EncryptionProgressArgs(0));

            string sZippedFile = Zip();
            if (string.IsNullOrEmpty(sZippedFile))
            {
                if(bNotifyProgress)
                {
                    OnEncryptAsyncProgess(context,
                        new EncryptionProgressArgs(-1, new Exception("Error when zipping files.")));
                    return;
                }
                else
                    throw new ArgumentException("Files to be encrypted cannot be accessed.");
            }

            if (bNotifyProgress)
                OnEncryptAsyncProgess(context, new EncryptionProgressArgs(30));

            string sEncryptedFile = Path.GetTempFileName();
            byte[] arrKey = null, arrIV = null;

            if (DESEncrypt(sZippedFile, sEncryptedFile, out arrKey, out arrIV))
            {
                if (bNotifyProgress)
                    OnEncryptAsyncProgess(context, new EncryptionProgressArgs(70));

                WriteEncryptedFile(CreateHeader(arrKey, arrIV),
                    sEncryptedFile, OutputFile);

                if (bNotifyProgress)
                    OnEncryptAsyncProgess(context, new EncryptionProgressArgs(80));
            }

            try
            {
                if (Zipped || Files.Count > 1)
                {
                    File.Delete(sZippedFile);
                }
                File.Delete(sEncryptedFile);
            }
            catch (Exception)
            {
                Debug.WriteLine("Error when deleting temp files in Encryptor.Encrypt().");
            }

            if (bNotifyProgress)
                OnEncryptAsyncProgess(context, new EncryptionProgressArgs(100));
        }

        protected void EncryptThread(object param)
        {
            try
            {
                DoEncrypt((Control)param, true);
            }
            catch (System.Exception ex)
            {
                OnEncryptAsyncProgess((Control)param, new EncryptionProgressArgs(-1, ex));	
            }
            
        }

        #endregion
    }
}
